AWS Transfer Family 経由で ls コマンドを実行時、Permission deniedエラーの解決方法
はじめに
AWS Transfer Family for SFTPサーバー経由でls コマンドを実行すると、「Permission denied」エラーが発生した場合の解決方法について説明します。
実際のエラー内容は下記の通りです。
$ sftp -i key名 ユーザー名@transfer familyエンドポイント sftp> pwd Remote working directory: /s3バケット名 sftp> ls Couldn't read directory: Permission denied sftp> ls test1 test1-1.txt test1-2.txt
S3バケット配下でlsコマンド実行時、Permission deniedとなり、test1
に対して実行するとファイル一覧が確認できます。
S3バケット配下は、AWSマネジメントコンソール上では下記の階層のように見えます。S3は、階層的フォルダ構造ではないため、実際は異なります。
. ├── test.png ├── test1 │ ├── test1-1.txt │ └── test1-2.txt └── test2 └── test2-1.txt
Transfer Familyのユーザー用のIAMロールのポリシーは、下記の通りです
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::s3バケット名" ] }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "s3:PutObject<em>", "s3:GetObject</em>", "s3:DeleteObject*" ], "Resource": [ "arn:aws:s3:::s3バケット名/test1/" ] } ] }
ファイル一覧が確認できるようにS3バケット配下に対して、ListBucketを許可し、test1
配下に対して、PutObject、GetObjectなどを許可しています。
Transfer Familyの仕様は下記の通りです
- プロトコル:SFTP
- ファイル転送先:S3バケット
- エンドポイントタイプ:パブリック
- ユーザー管理方法:サービスマネージド
- ユーザーのホームディレクトリは、S3バケット
結論
lsコマンド実行時に「Permission denied」エラーが発生するのは、以下の2つの条件に該当する場合です。 いずれかの条件に当てはまらなければ、エラーは解決します。
- フォルダオブジェクト (キー名の末尾が / である 0 バイトのオブジェクト) が存在する、かつ、フォルダオブジェクトに対するGetObjectを許可していない
- Transfer Familyの最適化されたディレクトリ設定が無効化
1点目に関しては、AWSドキュメントに記載がありました。
Transfer Family がファイルの書き込みまたは一覧表示のHeadObject呼び出しを行う必要がある場合、この呼び出しにはGetObject権限が必要なため、「アクセスが拒否されました」というエラーで失敗します。 この場合、/で終わるすべてのオブジェクトにアクセス許可を追加するポリシー条件を追加することで、アクセスを GetObject 許可できます。これにより、GetObject ファイルが読み取られないようにすることができますが、ユーザーはフォルダーを一覧表示したり、フォルダーを移動したりすることができます 引用
ls コマンドを実行し、一部の結果が Amazon S3 ゼロバイト オブジェクトである場合 (これらのオブジェクトにはスラッシュ文字で終わるキーがあります)、Transfer Familyはこれらの各オブジェクトに対する HeadObject リクエストを発行します引用
今回のエラーに当てはめると、lsコマンド実行時にHeadObjectが行われますが、IAMロールにはフォルダオブジェクトに対するGetObject権限が設定されていないため、「Permission denied」となりました。後述しますが、フォルダオブジェクトが存在しない場合は、「Permission denied」エラーは発生しません。
2点目は、Transfer Familyの最適化されたディレクトリ設定が有効化されている場合は、HeadObjectが実行されないため、GetObject の許可が不要になり、ls コマンドが実行できます。執筆時点では、ドキュメントには記載がなかったため、実際に試した上での結論です。試した内容は後述します。
S3のオブジェクトとフォルダ
S3バケットでは、データは、オブジェクトとして保存され、それぞれが一意の識別子であるキーによって参照されます。
ファイルシステムと異なり、S3はフラットな構造を持ち、階層的フォルダ構造ではありません。
たとえば、images/holiday/photo.jpg
というキーがあった場合、images/
というフォルダおよび images/holiday/
というサブフォルダに配置されているように見えますが、これらはS3のフラットなストレージ構造上のため実際には存在しません。この場合のスラッシュ(/)は単なる文字です。
しかし、オブジェクトをグループ化するためのフォルダの概念はサポートされています。
このフォルダは、キーに含まれるスラッシュ(/)によって形成される階層的な名前空間です。
S3のコンソールなどでフォルダを作成すると、「指定したフォルダ名 + 末尾がスラッシュ(/)」である0バイトのオブジェクト(以降、フォルダオブジェクト)が作成されます。
例えば、バケット内にphotos
という名前のフォルダを作成すると、photos/
というキーで0バイトのオブジェクトが作成され、管理コンソール上ではphotos
がフォルダとして表示されます。この場合のスラッシュ(/)はオブジェクト名を意味します。
例として、フォルダオブジェクトが作成される場合は、以下のケースです。
- コンソール上でフォルダを作成する。
- S3にアップロードするときに、フォルダごとアップロードする
aws s3 cp ~/directory s3://bucket-name/ --recursive
以下のように、S3にアップロードするとき、プレフィックス (接頭辞) を指定してアップロードする場合は、フォルダオブジェクトは作成されません。
$ aws s3 cp test.txt s3://bucket-name/prefix
HeadObjectについて
HeadObjectは、オブジェクトからメタデータを取得するAPIです。
HeadObjectを実行した際、リクエストしたオブジェクトの有無と権限によって返すステータスコードが変わります。
- リクエストしたオブジェクトが存在する
- s3:GetObject の権限がある場合、200 OKを返す
- s3:GetObject の権限がない場合、403 Forbiddenを返す
- リクエストしたオブジェクトが存在しない
- s3:ListBucket の権限がある場合、404 Not Foundを返す
- s3:ListBucket の権限がない場合、403 Forbiddenを返す
つまり、ls コマンドを実行すると、ListObjectsが行われたのち、末尾がスラッシュ(/)のオブジェクトに対しては、HeadObjectが行われ、上記の上限分岐から返されるステータスコードが変わります。
フォルダオブジェクトが存在する場合と存在しない場合で、ListObjects と HeadObject の挙動を解説します。
フォルダオブジェクトが存在する場合
フォルダオブジェクトはコンソール上ではなく、以下のコマンドを使用して確認できます。
$ aws s3 ls --recursive s3://S3バケット名 時刻 バイト数 オブジェクト 省略 100 test.png 省略 0 test1/ 省略 10 test1/test1-1.txt 省略 50 test1/test1-2.txt 省略 0 test2/ 省略 50 test2/test2-1.txt
test1/
とtest2/
は、0バイトのため、フォルダオブジェクトです。
バケット直下に対して、Transfer Family 経由でls コマンドを実行しますと、Transfer Family によって以下の API がリクエストされます。
sftp> pwd Remote working directory: /s3バケット名 sftp> ls Couldn't read directory: Permission denied
- ListObjects
- s3:ListBucketが許可されているため、バケット内のオブジェクト一覧が返されます。
- HeadObject (Key: test1/)
- フォルダオブジェクト
test1/
は存在し、s3:GetObject が許可されているため、200 OK が返されます
- フォルダオブジェクト
- HeadObject (Key: test2/)
- フォルダオブジェクト
test2/
は存在するものの、s3:GetObject が許可されていないため、403 Forbidden が返され、Permission deniedが表示されます。
- フォルダオブジェクト
Transfer Family for SFTP サーバーに対して各種SFTPコマンドを実行した際、S3バケットに送信されるリクエストが決定される方法について、ドキュメントに網羅的な情報は掲載されておらず、執筆時点での動作仕様としてご認識下さい。
フォルダオブジェクトが存在しない場合
以下の場合、フォルダオブジェクトが存在しておりません。
$ aws s3 ls --recursive s3://S3バケット名 時刻 バイト数 オブジェクト 省略 100 test.png 省略 10 test1/test1-1.txt 省略 50 test1/test1-2.txt 省略 50 test2/test2-1.txt
Transfer Family サーバー経由で ls コマンドを実行すると、Transfer Familyは、「/」を含むオブジェクト名を仮想的にフォルダとして解釈・表示します。
sftp> pwd Remote working directory: /s3バケット名 sftp> ls test.png test1 test2
フォルダが存在するように見える理由は、バケットに対する ListObjects 実行の結果、バケット内にtest1/
や test2/
をプレフィックスに持つキー名のオブジェクトが含まれているためです。
- test1/test1-1.txt
- test1/test1-2.txt
- test2/test2-1.txt
これらのオブジェクトが存在しているため、Transfer Family によって S3 バケット内での階層構造の分析が行われ、バケット直下に対して ls コマンドを実行した場合には test1/
や test2/
といったフォルダが存在するように表示をされます。
バケット直下に対して ls コマンドを実行すると、Transfer Family によって下記の API がリクエストされます。
- ListObjects
- s3:ListBucketが許可されているため、バケット内のオブジェクト一覧(
test1/test1-2.txt
とtest1/test1-2.txt
、test2/test2-1.txt
)が返されます。
- s3:ListBucketが許可されているため、バケット内のオブジェクト一覧(
- HeadObject (Key: test1/)
test1/
は、フォルダオブジェクトでないため、404 Not Found が返されます。「1. ListObjects」の実行結果内容を元に仮想フォルダとしてtest1
が表示されますtest2/
は、フォルダオブジェクトでないため、404 Not Found が返されます。「1. ListObjects」の実行結果内容を元に仮想フォルダとしてtest2
が表示されます
HeadObjectの確認
ls コマンド実行時にHeadObjectが実行されているか、Permission deniedエラー時のS3アクセスログをもとに確認します。分析方法は、下記の記事を参考にしました。
同時刻に下記のログが確認できました。
operation | key | request_uri | httpstatus | errorcode | objectsize |
---|---|---|---|---|---|
REST.GET.BUCKET | - | "GET /?list-type=2&delimiter=%2F&max-keys=1000&prefix=&encoding-type=url&fetch-owner=false HTTP/1.1" | 200 | - | |
REST.HEAD.OBJECT | test2/ | "HEAD /test2/ HTTP/1.1" | 403 | AccessDenied | |
REST.HEAD.OBJECT | test1/ | "HEAD /test1/ HTTP/1.1" | 200 | - | 0 |
ListObjects と HeadObject(HEAD
)が実行されていることがわかります。
そのため、ls コマンド実行時には、s3:ListBucket
とs3:GetObject
を許可する必要があります。
ちなみに、フォルダオブジェクトではない場合、下記の通り404のステータスコードになります。
operation | key | request_uri | httpstatus | errorcode | objectsize |
---|---|---|---|---|---|
REST.HEAD.OBJECT | test2/ | "HEAD /test2/ HTTP/1.1" | 404 | NoSuchKey |
試してみた
再度述べますが、以下の2つの条件に該当する場合にlsコマンドを実行すると「Permission denied」エラーが発生しますので、いずれか条件に当てはまらない場合、エラーが回避されるかを確認してみます。
- フォルダオブジェクト (キー名の末尾が / である 0 バイトのオブジェクト) が存在する、かつ、フォルダオブジェクトに対するGetObjectを許可していない
- 1.GetObjectを許可してみる
- Transfer Familyの最適化されたディレクトリ設定が無効化
- 2.最適化されたディレクトリを有効化してみる
1.GetObjectを許可
ユーザー用のIAMロールは、Transfer Familyのユーザーで設定しています。そのため、ユーザー用のIAMロールにGetObjectを追加します。
再掲ですが、S3バケット配下は、AWSマネジメントコンソール上では下記の階層のように見えます。S3は、階層的フォルダ構造ではないため、実際は異なります。
. ├── test.png ├── test1 │ ├── test1-1.txt │ └── test1-2.txt └── test2 └── test2-1.txt
test1
配下に対して、すでにGetObjectが許可されています。test2
配下に対してGetObjectを許可します。
下記の通り追加しました。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::s3バケット名" ] }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "s3:PutObject<em>", "s3:GetObject</em>", "s3:DeleteObject<em>" ], "Resource": [ "arn:aws:s3:::s3バケット名/test1/</em>" ] }, { "Sid": "VisualEditor2", "Effect": "Allow", "Action": "s3:GetObject*", "Resource": "arn:aws:s3:::s3バケット名/test2/" } ] }
実際にSFTP接続して試してみます。
$ sftp> ls test.png test1 test2
lsを実行するとファイル一覧が確認できました。
2.最適化されたディレクトリを有効化
Transfer Familyの詳細設定を確認すると、、「最適化されたディレクトリ」が無効になっていることが分かります。
有効化します。
有効化後、サーバーの再起動は不要です。
$ sftp> ls test.png test1 test2
ls コマンドを実行するとファイル一覧が確認できました。
S3のアクセスログからAthenaで分析したところ、下記の通り、HeadObjectは実行されないことが確認できました。
operation | key | request_uri | httpstatus | errorcode | objectsize |
---|---|---|---|---|---|
REST.GET.BUCKET | - | "GET /?list-type=2&delimiter=%2F&max-keys=1000&prefix=&encoding-type=url&fetch-owner=false HTTP/1.1" | 200 | - |
S3 バケット直下に対する ls コマンドを実行した場合、HeadObject は行われず、ListObjects のみが実行されるため、s3:GetObject の権限を付与していない場合にも成功するが分かりました。